#! /usr/bin/env python

#make executable with: chmod +x myscript.py
#run with: ~/Desktop/bassline.py

import random
import os
import roman
import re
#import math

MIDI_MAX=2**7-1
pth="/Users/eflister/Desktop/pyBass" #"~/Desktop/" tilda not supported?

def resolve(phrases,key,r=[]):
	if r==[]:
		r=re.compile("^(?P<function>[(?iIV)]+)(?P<quality>.*)$")
	for i in range(len(phrases)):
		if type(phrases[i])==str:
			m=r.match(phrases[i])
			if m.group("function").islower():
				mode="min"
			elif m.group("function").isupper():
				mode="maj"
			else:
				raise ValueError, 'found mixed case roman numeral: {0}'.format(m.group("function"))
			phrases[i]=key+"("+str(roman.roman_to_int(m.group("function")))+mode+")"+m.group("quality")
		else:
			resolve(phrases[i],key,r)

def bassline(fileName="blues.txt",key="C",beatsPerMeasure=4,tempo=100,swing=True,midiPort=0,midiChannel=0):
	try:
		os.chdir(pth)
		with open(fileName,'r') as f:  #syntax in 2.6 but not 2.3.5 (builtin to osx 10.4.11)?  need http://www.python.org/ftp/python/2.6/python-2.6-macosx.dmg
			for line in f:
				print line,
	except BaseException as e:  #syntax in 2.6 but not 2.3.5?
		print e
	print '\n'

	#replace following with file parser above
	phrases=[["I7", "IV7", "I7", "I7"], ["IV7", "IV7", "I7", "I7"], ["V7", "IV7", "I7", ["iib57", "V7"]]] 
	vamp=["V7"]

	resolve(phrases,key)
	print phrases

	class State: pass

	state=State()

	state.swing=swing
	state.dynamic=MIDI_MAX/2
	state.currNote=[]

	chorus=0
	done=False
	while not done:
		if chorus !=0 and random.random()>.2:
			state.business+=(1-state.business)*min(.25,random.random())
		else:
			state.business=min(.5,random.random())

		chorus+=1
		print "***chorus " + str(chorus) + " business: " + str(state.business)
		for phrase in range(len(phrases)):
			phraseDone=False
			measure,beat=1,1
			if phrase+1>=len(phrases):
				nextPhrase=0
			else:
				nextPhrase=phrase+1
			state.nextPhraseFirstMeasure=phrases[nextPhrase][0]
	
			while not phraseDone:
				beat+=2 #causes beatsPerMeasure<=2 to be wrong!
				if beat>beatsPerMeasure:
					measure,beat=measure+1,1
				if measure>len(phrases[phrase]):
					phraseDone=True
				state=chooseNotes(phrases[phrase],measure,beat,beatsPerMeasure,state)
		done=raw_input('\ncontinue? (n to stop)')=='n'

def chooseNotes(phrase,measure,beat,beatsPerMeasure,state): #why do i have to pass these in -- i thought it would look into caller's scope?
	out=[]
	if state.currNote==[]: #beginning of song
		state.currNote=getDegree(phrase[measure-1],1,beatsPerMeasure,1)
		print '\n['+state.currNote+' ',
	
	newMeasure=beat==1;
	done=False
	wrap=" "
	while not done:
		if measure>len(phrase) and beat==1 and out==[]:
			wrap="\n"
			m=state.nextPhraseFirstMeasure
		else:
			m=phrase[measure-1]
		out.insert(0,getDegree(m,beat,beatsPerMeasure,1))

		beat-=1
		if beat<1:
			beat=beatsPerMeasure
			measure-=1
		done = float(beat)/2 > float(beat)//2 #previous odd beat

	state.currNote=out[len(out)-1]
	for b in range(len(out)):
		if newMeasure and b==len(out)-1:
			print ']'+wrap+'[',
		print out[b]+' ',
	if state.swing:
		dynamics=[1, 0, 3, 2, 0, 3]
	else:
		dynamics=[1, 0, 3, 0, 2, 0, 3, 0]

	return state

def getDegree(measure,beat,beatsPerMeasure,degree):
	if type(measure)==str:
		m=measure
	else:
		m=measure[int((float(beat)-1)/beatsPerMeasure*len(measure))]
	return m+'('+str(degree)+')'

bassline()